]> dgit.raspbian.org Git - gnome-shell.git/commitdiff
NetworkAgent: add support for VPN connections
authorGiovanni Campagna <gcampagna@src.gnome.org>
Thu, 3 Nov 2011 20:57:33 +0000 (21:57 +0100)
committerGiovanni Campagna <gcampagna@src.gnome.org>
Tue, 14 Feb 2012 18:16:28 +0000 (19:16 +0100)
VPN secrets are stored by the plugins, that provide separate
helpers for authentication. This commit adds the support for invoking
the binaries and pass them connection details.
For plugins that support it (as exposed by their keyfile), we invoke
them in "external-ui-mode" and expect a set of metadata about the
secrets which is used to build a shell styled dialog.

https://bugzilla.gnome.org/show_bug.cgi?id=658484

js/ui/networkAgent.js
src/shell-network-agent.c
src/shell-network-agent.h
src/shell-util.c
src/shell-util.h

index 2f8848098383920101a0f170209d7dfbe64ee4b1..10b090b6ceb807c6f62af15a7387bed1a3d6536c 100644 (file)
@@ -21,6 +21,8 @@
 
 const Clutter = imports.gi.Clutter;
 const Gio = imports.gi.Gio;
+const GLib = imports.gi.GLib;
+const GObject = imports.gi.GObject;
 const Lang = imports.lang;
 const NetworkManager = imports.gi.NetworkManager;
 const NMClient = imports.gi.NMClient;
@@ -28,15 +30,18 @@ const Pango = imports.gi.Pango;
 const Shell = imports.gi.Shell;
 const St = imports.gi.St;
 
+const Config = imports.misc.config;
 const ModalDialog = imports.ui.modalDialog;
 const PopupMenu = imports.ui.popupMenu;
 const ShellEntry = imports.ui.shellEntry;
 
+const VPN_UI_GROUP = 'VPN Plugin UI';
+
 const NetworkSecretDialog = new Lang.Class({
     Name: 'NetworkSecretDialog',
     Extends: ModalDialog.ModalDialog,
 
-    _init: function(agent, requestId, connection, settingName, hints) {
+    _init: function(agent, requestId, connection, settingName, hints, contentOverride) {
         this.parent({ styleClass: 'prompt-dialog' });
 
         this._agent = agent;
@@ -45,7 +50,10 @@ const NetworkSecretDialog = new Lang.Class({
         this._settingName = settingName;
         this._hints = hints;
 
-        this._content = this._getContent();
+        if (contentOverride)
+            this._content = contentOverride;
+        else
+            this._content = this._getContent();
 
         let mainContentBox = new St.BoxLayout({ style_class: 'prompt-dialog-main-layout',
                                                 vertical: false });
@@ -174,14 +182,14 @@ const NetworkSecretDialog = new Lang.Class({
         }
 
         if (valid) {
-            this._agent.respond(this._requestId, false);
+            this._agent.respond(this._requestId, Shell.NetworkAgentResponse.CONFIRMED);
             this.close(global.get_current_time());
         }
         // do nothing if not valid
     },
 
     cancel: function() {
-        this._agent.respond(this._requestId, true);
+        this._agent.respond(this._requestId, Shell.NetworkAgentResponse.USER_CANCELED);
         this.close(global.get_current_time());
     },
 
@@ -357,6 +365,240 @@ const NetworkSecretDialog = new Lang.Class({
     }
 });
 
+const VPNRequestHandler = new Lang.Class({
+    Name: 'VPNRequestHandler',
+
+    _init: function(agent, requestId, authHelper, serviceType, connection, hints, flags) {
+        this._agent = agent;
+        this._requestId = requestId;
+        this._connection = connection;
+        this._pluginOutBuffer = [];
+        this._title = null;
+        this._description = null;
+        this._content = [ ];
+        this._shellDialog = null;
+
+        let connectionSetting = connection.get_setting_connection();
+
+        let argv = [ authHelper.fileName,
+                     '-u', connectionSetting.uuid,
+                     '-n', connectionSetting.id,
+                     '-s', serviceType
+                   ];
+        if (authHelper.externalUIMode)
+            argv.push('--external-ui-mode');
+        if (flags & NMClient.SecretAgentGetSecretsFlags.ALLOW_INTERACTION)
+            argv.push('-i');
+        if (flags & NMClient.SecretAgentGetSecretsFlags.REQUEST_NEW)
+            argv.push('-r');
+
+        this._newStylePlugin = authHelper.externalUIMode;
+
+        try {
+            let [success, pid, stdin, stdout, stderr] =
+                GLib.spawn_async_with_pipes(null, /* pwd */
+                                            argv,
+                                            null, /* envp */
+                                            GLib.SpawnFlags.DO_NOT_REAP_CHILD,
+                                            null /* child_setup */);
+
+            this._childPid = pid;
+            this._stdin = new Gio.UnixOutputStream({ fd: stdin, close_fd: true });
+            this._stdout = new Gio.UnixInputStream({ fd: stdout, close_fd: true });
+            // We need this one too, even if don't actually care of what the process
+            // has to say on stderr, because otherwise the fd opened by g_spawn_async_with_pipes
+            // is kept open indefinitely
+            let stderrStream = new Gio.UnixInputStream({ fd: stderr, close_fd: true });
+            stderrStream.close(null);
+            this._dataStdout = new Gio.DataInputStream({ base_stream: this._stdout });
+
+            if (this._newStylePlugin)
+                this._readStdoutNewStyle();
+            else
+                this._readStdoutOldStyle();
+
+            this._childWatch = GLib.child_watch_add(GLib.PRIORITY_DEFAULT, pid,
+                                                    Lang.bind(this, this._vpnChildFinished));
+
+            this._writeConnection();
+        } catch(e) {
+            logError(e, 'error while spawning VPN auth helper');
+
+            this._agent.respond(requestId, Shell.NetworkAgentResponse.INTERNAL_ERROR);
+        }
+    },
+
+    cancel: function() {
+        if (this._newStylePlugin && this._shellDialog) {
+            this._shellDialog.close(global.get_current_time());
+            this._shellDialog.destroy();
+        } else {
+            try {
+                this._stdin.write('QUIT\n\n', null);
+            } catch(e) { /* ignore broken pipe errors */ }
+        }
+
+        this.destroy();
+    },
+
+    destroy: function() {
+        if (this._destroyed)
+            return;
+
+        GLib.source_remove(this._childWatch);
+
+        this._stdin.close(null);
+        // Stdout is closed when we finish reading from it
+
+        this._destroyed = true;
+    },
+
+    _vpnChildFinished: function(pid, status, requestObj) {
+        if (this._newStylePlugin) {
+            // For new style plugin, all work is done in the async reading functions
+            // Just reap the process here
+            return;
+        }
+
+        let [exited, exitStatus] = Shell.util_wifexited(status);
+
+        if (exited) {
+            if (exitStatus != 0)
+                this._agent.respond(this._requestId, Shell.NetworkAgentResponse.USER_CANCELED);
+            else
+                this._agent.respond(this._requestId, Shell.NetworkAgentResponse.CONFIRMED);
+        } else
+            this._agent.respond(this._requestId, Shell.NetworkAgentResponse.INTERNAL_ERROR);
+
+        this.destroy();
+    },
+
+    _vpnChildProcessLineOldStyle: function(line) {
+        if (this._previousLine != undefined) {
+            // Two consecutive newlines mean that the child should be closed
+            // (the actual newlines are eaten by Gio.DataInputStream)
+            // Send a termination message
+            if (line == '' && this._previousLine == '') {
+                try {
+                    this._stdin.write('QUIT\n\n', null);
+                } catch(e) { /* ignore broken pipe errors */ }
+            } else {
+                this._agent.set_password(this._requestId, this._previousLine, line);
+                this._previousLine = undefined;
+            }
+        } else {
+            this._previousLine = line;
+        }
+    },
+
+    _readStdoutOldStyle: function() {
+        this._dataStdout.read_line_async(GLib.PRIORITY_DEFAULT, null, Lang.bind(this, function(stream, result) {
+            let [line, len] = this._dataStdout.read_line_finish_utf8(result);
+
+            if (line == null) {
+                // end of file
+                this._stdout.close(null);
+                return;
+            }
+
+            this._vpnChildProcessLineOldStyle(line);
+
+            // try to read more!
+            this._readStdoutOldStyle();
+        }));
+    },
+
+    _readStdoutNewStyle: function() {
+        this._dataStdout.fill_async(-1, GLib.PRIORITY_DEFAULT, null, Lang.bind(this, function(stream, result) {
+            let cnt = this._dataStdout.fill_finish(result);
+
+            if (cnt == 0) {
+                // end of file
+                this._showNewStyleDialog();
+
+                this._stdout.close(null);
+                return;
+            }
+
+            // Try to read more
+            this._dataStdout.set_buffer_size(2 * this._dataStdout.get_buffer_size());
+            this._readStdoutNewStyle();
+        }));
+    },
+
+    _showNewStyleDialog: function() {
+        let keyfile = new GLib.KeyFile();
+        let contentOverride;
+
+        try {
+            keyfile.load_from_data(this._dataStdout.peek_buffer(),
+                                   GLib.KeyFileFlags.NONE);
+
+            if (keyfile.get_integer(VPN_UI_GROUP, 'Version') != 2)
+                throw new Error('Invalid plugin keyfile version, is %d');
+
+            contentOverride = { title: keyfile.get_string(VPN_UI_GROUP, 'Title'),
+                                message: keyfile.get_string(VPN_UI_GROUP, 'Description'),
+                                secrets: [] };
+
+            let [groups, len] = keyfile.get_groups();
+            for (let i = 0; i < groups.length; i++) {
+                if (groups[i] == VPN_UI_GROUP)
+                    continue;
+
+                let value = keyfile.get_string(groups[i], 'Value');
+                let shouldAsk = keyfile.get_boolean(groups[i], 'ShouldAsk');
+
+                if (shouldAsk) {
+                    contentOverride.secrets.push({ label: keyfile.get_string(groups[i], 'Label'),
+                                                   key: groups[i],
+                                                   value: value,
+                                                   password: keyfile.get_boolean(groups[i], 'IsSecret')
+                                                 });
+                } else {
+                    if (!value.length) // Ignore empty secrets
+                        continue;
+
+                    this._agent.set_password(this._requestId, groups[i], value);
+                }
+            }
+        } catch(e) {
+            logError(e, 'error while reading VPN plugin output keyfile');
+
+            this._agent.respond(this._requestId, Shell.NetworkAgentResponse.INTERNAL_ERROR);
+            return;
+        }
+
+        if (contentOverride.secrets.length) {
+            // Only show the dialog if we actually have something to ask
+            this._shellDialog = new NetworkSecretDialog(this._agent, this._requestId, this._connection, 'vpn', [], contentOverride);
+            this._shellDialog.open(global.get_current_time());
+        } else {
+            this._agent.respond(this._requestId, Shell.NetworkAgentResponse.CONFIRMED);
+        }
+    },
+
+    _writeConnection: function() {
+        let vpnSetting = this._connection.get_setting_vpn();
+
+        try {
+            vpnSetting.foreach_data_item(Lang.bind(this, function(key, value) {
+                this._stdin.write('DATA_KEY=' + key + '\n', null);
+                this._stdin.write('DATA_VAL=' + (value || '') + '\n\n', null);
+            }));
+            vpnSetting.foreach_secret(Lang.bind(this, function(key, value) {
+                this._stdin.write('SECRET_KEY=' + key + '\n', null);
+                this._stdin.write('SECRET_VAL=' + (value || '') + '\n\n', null);
+            }));
+            this._stdin.write('DONE\n\n', null);
+        } catch(e) {
+            logError(e, 'internal error while writing connection to helper');
+
+            this._agent.respond(this._requestId, Shell.NetworkAgentResponse.INTERNAL_ERROR);
+        }
+    },
+});
+
 const NetworkAgent = new Lang.Class({
     Name: 'NetworkAgent',
 
@@ -365,11 +607,18 @@ const NetworkAgent = new Lang.Class({
                                                 identifier: 'org.gnome.Shell.NetworkAgent' });
 
         this._dialogs = { };
+        this._vpnRequests = { };
+
         this._native.connect('new-request', Lang.bind(this, this._newRequest));
         this._native.connect('cancel-request', Lang.bind(this, this._cancelRequest));
     },
 
-    _newRequest:  function(agent, requestId, connection, settingName, hints) {
+    _newRequest:  function(agent, requestId, connection, settingName, hints, flags) {
+        if (settingName == 'vpn') {
+            this._vpnRequest(requestId, connection, hints, flags);
+            return;
+        }
+
         let dialog = new NetworkSecretDialog(agent, requestId, connection, settingName, hints);
         dialog.connect('destroy', Lang.bind(this, function() {
             delete this._dialogs[requestId];
@@ -379,7 +628,74 @@ const NetworkAgent = new Lang.Class({
     },
 
     _cancelRequest: function(agent, requestId) {
-        this._dialogs[requestId].close(global.get_current_time());
-        this._dialogs[requestId].destroy();
+        if (this._dialogs[requestId]) {
+            this._dialogs[requestId].close(global.get_current_time());
+            this._dialogs[requestId].destroy();
+            delete this._dialogs[requestId];
+        } else if (this._vpnRequests[requestId]) {
+            this._vpnRequests[requestId].cancel();
+            delete this._vpnRequests[requestId];
+        }
+    },
+
+    _vpnRequest: function(requestId, connection, hints, flags) {
+        let vpnSetting = connection.get_setting_vpn();
+        let serviceType = vpnSetting.service_type;
+
+        this._buildVPNServiceCache();
+
+        let binary = this._vpnBinaries[serviceType];
+        if (!binary) {
+            log('Invalid VPN service type (cannot find authentication binary)');
+
+            /* cancel the auth process */
+            this._native.respond(requestId, Shell.NetworkAgentResponse.INTERNAL_ERROR);
+            return;
+        }
+
+        this._vpnRequests[requestId] = new VPNRequestHandler(this._native, requestId, binary, serviceType, connection, hints, flags);
+    },
+
+    _buildVPNServiceCache: function() {
+        if (this._vpnCacheBuilt)
+            return;
+
+        this._vpnCacheBuilt = true;
+        this._vpnBinaries = { };
+
+        let dir = Gio.file_new_for_path(GLib.build_filenamev([Config.SYSCONFDIR, 'NetworkManager/VPN']));
+        try {
+            let fileEnum = dir.enumerate_children('standard::name', Gio.FileQueryInfoFlags.NONE, null);
+            let info;
+
+            while ((info = fileEnum.next_file(null))) {
+                let name = info.get_name();
+                if (name.substr(-5) != '.name')
+                    continue;
+
+                try {
+                    let keyfile = new GLib.KeyFile();
+                    keyfile.load_from_file(dir.get_child(name).get_path(), GLib.KeyFileFlags.NONE);
+                    let service = keyfile.get_string('VPN Connection', 'service');
+                    let binary = keyfile.get_string('GNOME', 'auth-dialog');
+                    let externalUIMode = false;
+                    try {
+                        externalUIMode = keyfile.get_boolean('GNOME', 'external-ui-mode');
+                    } catch(e) { } // ignore errors if key does not exist
+                    let path = GLib.build_filenamev([Config.LIBEXECDIR, binary]);
+
+                    if (GLib.file_test(path, GLib.FileTest.IS_EXECUTABLE))
+                        this._vpnBinaries[service] = { fileName: path, externalUIMode: externalUIMode };
+                    else
+                        throw new Error('VPN plugin at %s is not executable'.format(path));
+                } catch(e) {
+                    log('Error \'%s\' while processing VPN keyfile \'%s\''.
+                        format(e.message, dir.get_child(name).get_path()));
+                    continue;
+                }
+            }
+        } catch(e) {
+            logError(e, 'error while enumerating VPN auth helpers');
+        }
     }
 });
index 0e547aaa840d87aaa0c0538075b146ca06296aed..b24999b7502722fd279df0340f3b6f35fe24db7c 100644 (file)
@@ -22,6 +22,7 @@
 #include "config.h"
 #include <string.h>
 #include <gnome-keyring.h>
+#include <dbus/dbus-glib.h>
 
 #include "shell-network-agent.h"
 
@@ -47,6 +48,8 @@ typedef struct {
 
   /* <gchar *setting_key, gchar *secret> */
   GHashTable                    *entries;
+  GHashTable                    *vpn_entries;
+  gboolean                       is_vpn;
 } ShellAgentRequest;
 
 struct _ShellNetworkAgentPrivate {
@@ -68,7 +71,6 @@ shell_agent_request_free (gpointer data)
   g_object_unref (request->connection);
   g_free (request->setting_name);
   g_strfreev (request->hints);
-
   g_hash_table_destroy (request->entries);
 
   g_slice_free (ShellAgentRequest, request);
@@ -122,7 +124,8 @@ request_secrets_from_ui (ShellAgentRequest *request)
                  request->request_id,
                  request->connection,
                  request->setting_name,
-                 request->hints);
+                 request->hints,
+                 (int)request->flags);
 }
 
 static void
@@ -274,11 +277,17 @@ get_secrets_keyring_cb (GnomeKeyringResult  result,
               && (attr->type == GNOME_KEYRING_ATTRIBUTE_TYPE_STRING))
             {
               gchar *secret_name = g_strdup (attr->value.string);
-              GValue *secret_value = g_slice_new0 (GValue);
-              g_value_init (secret_value, G_TYPE_STRING);
-              g_value_set_string (secret_value, item->secret);
 
-              g_hash_table_insert (closure->entries, secret_name, secret_value);
+              if (!closure->is_vpn)
+                {
+                  GValue *secret_value = g_slice_new0 (GValue);
+                  g_value_init (secret_value, G_TYPE_STRING);
+                  g_value_set_string (secret_value, item->secret);
+
+                  g_hash_table_insert (closure->entries, secret_name, secret_value);
+                }
+              else
+                g_hash_table_insert (closure->vpn_entries, secret_name, g_strdup (item->secret));
 
               if (closure->hints)
                 n_found += strv_has (closure->hints, secret_name);
@@ -293,7 +302,6 @@ get_secrets_keyring_cb (GnomeKeyringResult  result,
   if (n_found == 0 &&
       (closure->flags & NM_SECRET_AGENT_GET_SECRETS_FLAG_ALLOW_INTERACTION))
     {
-      /* Even if n_found == 0, secrets is not necessarily empty */
       nm_connection_update_secrets (closure->connection, closure->setting_name, closure->entries, NULL);
 
       request_secrets_from_ui (closure);
@@ -327,17 +335,8 @@ shell_network_agent_get_secrets (NMSecretAgent                 *agent,
   NMSettingConnection *setting_connection;
   const char *connection_type;
 
-  /* VPN secrets are currently unimplemented - bail out early */
   setting_connection = nm_connection_get_setting_connection (connection);
   connection_type = nm_setting_connection_get_connection_type (setting_connection);
-  if (strcmp (connection_type, "vpn") == 0)
-    {
-      GError *error = g_error_new (NM_SECRET_AGENT_ERROR,
-                                   NM_SECRET_AGENT_ERROR_AGENT_CANCELED,
-                                   "VPN secrets are currently unhandled.");
-      callback (NM_SECRET_AGENT (self), connection, NULL, error, callback_data);
-      return;
-    }
 
   request = g_slice_new (ShellAgentRequest);
   request->self = g_object_ref (self);
@@ -347,8 +346,24 @@ shell_network_agent_get_secrets (NMSecretAgent                 *agent,
   request->flags = flags;
   request->callback = callback;
   request->callback_data = callback_data;
+  request->is_vpn = !strcmp(connection_type, NM_SETTING_VPN_SETTING_NAME);
+  request->keyring_op = NULL;
   request->entries = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, gvalue_destroy_notify);
 
+  if (request->is_vpn)
+    {
+      GValue *secret_value;
+
+      request->vpn_entries = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);
+
+      secret_value = g_slice_new0 (GValue);
+      g_value_init (secret_value, dbus_g_type_get_map ("GHashTable", G_TYPE_STRING, G_TYPE_STRING));
+      g_value_take_boxed (secret_value, request->vpn_entries);
+      g_hash_table_insert (request->entries, g_strdup(NM_SETTING_VPN_SECRETS), secret_value);
+    }
+  else
+    request->vpn_entries = NULL;
+
   request->request_id = g_strdup_printf ("%s/%s", connection_path, setting_name);
   g_hash_table_replace (self->priv->requests, request->request_id, request);
 
@@ -388,17 +403,24 @@ shell_network_agent_set_password (ShellNetworkAgent *self,
   priv = self->priv;
   request = g_hash_table_lookup (priv->requests, request_id);
 
-  value = g_slice_new0 (GValue);
-  g_value_init (value, G_TYPE_STRING);
-  g_value_set_string (value, setting_value);
+  if (!request->is_vpn)
+    {
+      value = g_slice_new0 (GValue);
+      g_value_init (value, G_TYPE_STRING);
+      g_value_set_string (value, setting_value);
 
-  g_hash_table_replace (request->entries, g_strdup (setting_key), value);
+      g_hash_table_replace (request->entries, g_strdup (setting_key), value);
+    }
+  else
+    {
+      g_hash_table_replace (request->vpn_entries, g_strdup (setting_key), g_strdup (setting_value));
+    }
 }
 
 void
-shell_network_agent_respond (ShellNetworkAgent *self,
-                             gchar             *request_id,
-                             gboolean           canceled)
+shell_network_agent_respond (ShellNetworkAgent         *self,
+                             gchar                     *request_id,
+                             ShellNetworkAgentResponse  response)
 {
   ShellNetworkAgentPrivate *priv;
   ShellAgentRequest *request;
@@ -410,7 +432,7 @@ shell_network_agent_respond (ShellNetworkAgent *self,
   priv = self->priv;
   request = g_hash_table_lookup (priv->requests, request_id);
 
-  if (canceled)
+  if (response == SHELL_NETWORK_AGENT_USER_CANCELED)
     {
       GError *error = g_error_new (NM_SECRET_AGENT_ERROR,
                                    NM_SECRET_AGENT_ERROR_USER_CANCELED,
@@ -422,10 +444,24 @@ shell_network_agent_respond (ShellNetworkAgent *self,
       return;
     }
 
+  if (response == SHELL_NETWORK_AGENT_INTERNAL_ERROR)
+    {
+      GError *error = g_error_new (NM_SECRET_AGENT_ERROR,
+                                   NM_SECRET_AGENT_ERROR_INTERNAL_ERROR,
+                                   "An internal error occurred while processing the request.");
+
+      request->callback (NM_SECRET_AGENT (self), request->connection, NULL, error, request->callback_data);
+      g_error_free (error);
+      g_hash_table_remove (priv->requests, request_id);
+      return;
+    }
+
+  /* response == SHELL_NETWORK_AGENT_CONFIRMED */
+
   /* Save updated secrets */
   dup = nm_connection_duplicate (request->connection);
-  nm_connection_update_secrets (dup, request->setting_name, request->entries, NULL);
 
+  nm_connection_update_secrets (dup, request->setting_name, request->entries, NULL);
   nm_secret_agent_save_secrets (NM_SECRET_AGENT (self), dup, NULL, NULL);
 
   outer = g_hash_table_new (g_str_hash, g_str_equal);
@@ -776,11 +812,12 @@ shell_network_agent_class_init (ShellNetworkAgentClass *klass)
                                              NULL, /* accu_data */
                                               NULL, /* marshaller */
                                              G_TYPE_NONE, /* return */
-                                             3, /* n_params */
+                                             5, /* n_params */
                                              G_TYPE_STRING,
                                              NM_TYPE_CONNECTION,
                                              G_TYPE_STRING,
-                                              G_TYPE_STRV);
+                                              G_TYPE_STRV,
+                                              G_TYPE_INT);
 
   signals[SIGNAL_CANCEL_REQUEST] = g_signal_new ("cancel-request",
                                                  G_TYPE_FROM_CLASS (klass),
index 179939a48f34bb173b407fb07b57250ed7290af2..e27b02eee1a5fd165f4247c69d1746b7e823e56e 100644 (file)
@@ -9,6 +9,12 @@
 
 G_BEGIN_DECLS
 
+typedef enum {
+  SHELL_NETWORK_AGENT_CONFIRMED,
+  SHELL_NETWORK_AGENT_USER_CANCELED,
+  SHELL_NETWORK_AGENT_INTERNAL_ERROR
+} ShellNetworkAgentResponse;
+
 typedef struct _ShellNetworkAgent         ShellNetworkAgent;
 typedef struct _ShellNetworkAgentClass    ShellNetworkAgentClass;
 typedef struct _ShellNetworkAgentPrivate  ShellNetworkAgentPrivate;
@@ -45,7 +51,7 @@ void               shell_network_agent_set_password (ShellNetworkAgent *self,
                                                      gchar             *setting_value);
 void               shell_network_agent_respond      (ShellNetworkAgent *self,
                                                      gchar             *request_id,
-                                                     gboolean           canceled);
+                                                     ShellNetworkAgentResponse response);
 
 /* If these are kept in sync with nm-applet, secrets will be shared */
 #define SHELL_KEYRING_UUID_TAG "connection-uuid"
index c389425ab75747fd9cec15a3d10f43fce8dee138..6647baf3d5d026da7f5f455bdc1de1e3d6f04028 100644 (file)
@@ -2,6 +2,9 @@
 
 #include "config.h"
 
+#include <sys/types.h>
+#include <sys/wait.h>
+
 #include "shell-util.h"
 #include <glib/gi18n-lib.h>
 #include <gtk/gtk.h>
@@ -861,3 +864,26 @@ shell_session_is_active_for_systemd (void)
   return TRUE;
 #endif
 }
+
+/**
+ * shell_util_wifexited:
+ * @status: the status returned by wait() or waitpid()
+ * @exit: (out): the actual exit status of the process
+ *
+ * Implements libc standard WIFEXITED, that cannot be used JS
+ * code.
+ * Returns: TRUE if the process exited normally, FALSE otherwise
+ */
+gboolean
+shell_util_wifexited (int  status,
+                      int *exit)
+{
+  gboolean ret;
+
+  ret = WIFEXITED(status);
+
+  if (ret)
+    *exit = WEXITSTATUS(status);
+
+  return ret;
+}
index 5de947342ae395a16b2b1110d8b08dec065948ae..5b93bd4b656162363d47eb190cbd2c2e88cf6eda 100644 (file)
@@ -52,6 +52,9 @@ void shell_shader_effect_set_double_uniform (ClutterShaderEffect *effect,
 
 gboolean shell_session_is_active_for_systemd (void);
 
+gboolean shell_util_wifexited                  (int               status,
+                                                int              *exit);
+
 G_END_DECLS
 
 #endif /* __SHELL_UTIL_H__ */